# Copyright 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import sys

import svn.core
import svn.client

from svn.core import SWIG_SVN_INVALID_REVNUM as SVN_INVALID_REVNUM

import gvn.changebranch
import gvn.cmdline
import gvn.errors
import gvn.svnauth
import gvn.util


helptext__gvn_log = """log: List locally modified files.
usage: log
"""


# the separator between log messages
SEP_STRING = \
  '------------------------------------------------------------------------\n'


class PlainReceiver(object):
  def __init__(self, ctx):
    self.ctx = ctx
    self.merge_stack = []

  def __call__(self, log_entry, pool):
    if callable(self.ctx.Cancel):
      self.ctx.Cancel()

    if log_entry.revision == 0 and log_entry.revprops is None:
      return

    if log_entry.revision == SVN_INVALID_REVNUM:
      self.merge_stack.pop()
      return

    author = log_entry.revprops.get('svn:author', '(no author)')

    date = log_entry.revprops.get('svn:date')
    if date is not None and len(date) > 0:
      # Convert date to a format for humans.
      tmp = svn.core.svn_time_from_cstring(date, pool)
      date = svn.core.svn_time_to_human_cstring(tmp, pool)
    else:
      date = '(no date)'

    message = log_entry.revprops.get('svn:log')
    if not self.ctx.options.quiet and message is None:
      message = ''

    sys.stdout.write('%sr%d | %s | %s'
                     % (SEP_STRING, log_entry.revision, author, date))
    if message is not None:
      lines = message.count('\n') + 1
      if lines == 1:
        plural = ''
      else:
        plural = 's'
      sys.stdout.write(' | %d line%s' % (lines, plural))
    sys.stdout.write('\n')
    if log_entry.changed_paths:
      sys.stdout.write('Changed paths:\n')
      for (path, change) in sorted(log_entry.changed_paths.iteritems(),
                                   key = lambda x: x[0]):
        if change.copyfrom_path is None:
          copy_data = ''
        else:
          copy_data = ' (from %s:%ld)' % (change.copyfrom_path,
                                          change.copyfrom_rev)
        sys.stdout.write('   %s %s%s\n' % (change.action, path, copy_data))

    # Print gvn metadata if any.
    approvers = []
    for (name, value) in log_entry.revprops.iteritems():
      if name == 'gvn:change':
        sys.stdout.write('From changebranch: %s\n' % (value,))
      elif name == 'gvn:submitted':
        sys.stdout.write('Submitted as: %s\n' % (value,))
      elif name.startswith('gvn:approve:'):
        approvers.append(name[len('gvn:approve:'):])
    if approvers:
      sys.stdout.write('Approved by: %s\n' % (', '.join(approvers),))

    merge_stack_len = len(self.merge_stack)
    if merge_stack_len > 0:
      # Print the result of merge line.
      sys.stdout.write('Merged via:')
      for (i, merged_rev) in enumerate(self.merge_stack):
        if i == merge_stack_len - 1:
          sep = '\n'
        else:
          sep = ','
        sys.stdout.write(' r%d%s' % (merged_rev, sep))

    if message is not None:
      sys.stdout.write('\n%s\n' % (message,))

    sys.stdout.flush()

    if log_entry.has_children:
      self.merge_stack.append(log_entry.revision)


def Handle_GvnLog(ctx):
  # TODO(epg): This is wrong, obviously.  Some commands (possibly only
  # log) take URLs or wc paths, maybe even intermixed.  The operand
  # scanning can just look for .startswith(//).
  ctx.wc_operands = False

  targets = []
  if ctx.options.change is not None:
    (username, cname, revision) = gvn.util.ParseChangeName(ctx.options.change)
    # XXX should be catching InvalidChangeName from ParseChangeName
    # but it's not raising it.
    if username is None and revision is None:
      revstr = ctx.options.change
    else:
      revstr = '%d:0' % (revision,)
      cbp = ctx.project.ChangeBranchPath(username, cname)
      targets = [ctx.project.repository.URL(cbp) + '@%d' % (revision,)]
  elif ctx.options.revision is not None:
    revstr = ctx.options.revision
  else:
    revstr = None

  # Build list of operands for code stolen from log-cmd.c
  if not targets:
    if ctx.operands:
      for i in ctx.operands:
        try:
          targets.append(ctx.project.repository.ShortToLongURL(i))
        except gvn.errors.NotShortURL:
          targets.append(i)
    else:
      # XXX should be path_dir once that's submitted
      targets = [ctx.path]

  # Build client context for stolen code.
  client = svn.client.create_context(ctx.pool)
  auth_baton = gvn.svnauth.GetAuthBaton(ctx.project_config.username,
                                        not ctx.options.non_interactive,
                                        ctx.config, ctx.pool)
  client.auth_baton = auth_baton[-1]

  # Assemble revision info for stolen code.
  start = svn.core.svn_opt_revision_t()
  end = svn.core.svn_opt_revision_t()
  start.kind = end.kind = svn.core.svn_opt_revision_unspecified
  if revstr is not None:
    svn.core.svn_opt_parse_revision(start, end, revstr)

  #################################################################
  # Begin theft from log-cmd.c.
  # See svn r29127/#3026.
#   SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
#                                                       opt_state->targets, 
#                                                       pool));

  target = targets[0]

  # Strip peg revision if targets contains an URI.
  peg_revision = svn.core.svn_opt_revision_t()
  true_path = svn.core.svn_opt_parse_path(peg_revision, target, ctx.pool)
  targets[0] = true_path

  if (start.kind != svn.core.svn_opt_revision_unspecified
      and end.kind == svn.core.svn_opt_revision_unspecified):
    # If the user specified exactly one revision, then start rev is
    # set but end is not.  We show the log message for just that
    # revision by making end equal to start.
    #
    # Note that if the user requested a single dated revision, then
    # this will cause the same date to be resolved twice.  The
    # extra code complexity to get around this slight inefficiency
    # doesn't seem worth it, however.
    end = start
  elif start.kind == svn.core.svn_opt_revision_unspecified:
    # Default to any specified peg revision.  Otherwise, if the
    # first target is an URL, then we default to HEAD:0.  Lastly,
    # the default is BASE:0 since WC@HEAD may not exist.
    if peg_revision.kind == svn.core.svn_opt_revision_unspecified:
      if svn.core.svn_path_is_url(target):
        start.kind = svn.core.svn_opt_revision_head
      else:
        start.kind = svn.core.svn_opt_revision_base
    else:
      start = peg_revision

    if end.kind == svn.core.svn_opt_revision_unspecified:
      end.kind = svn.core.svn_opt_revision_number
      end.value.number = 0

  if svn.core.svn_path_is_url(target):
    for target in targets[1:]:
      if svn.core.svn_path_is_url(target):
        raise svn.core.SubversionException(
          message='Only relative paths can be specified after a URL',
          apr_err=svn.core.SVN_ERR_UNSUPPORTED_FEATURE)

  receiver = PlainReceiver(ctx)

  # Get all revprops, including gvn:approve:*
  revprops = None

  svn.client.log4(targets, peg_revision, start, end,
                  int(ctx.options.limit), ctx.options.verbose,
                  ctx.options.stop_on_copy, ctx.options.use_merge_history,
                  revprops, receiver, client, ctx.pool)

  if not ctx.options.incremental:
    sys.stdout.write(SEP_STRING)

  return 0

gvn.cmdline.AddCommand('log', Handle_GvnLog, helptext__gvn_log,
                       [
  'change',
  'incremental',
  'limit',
  'project',
  'quiet',
  'revision',
  'stop-on-copy',
  'targets',
  'use-merge-history',
  'verbose',
])
